#
# RTEMS Tools Project (http://www.rtems.org/)
# Copyright 2010-2013 Chris Johns (chrisj@rtems.org)
# All rights reserved.
#
# This file is part of the RTEMS Tools package in 'rtems-tools'.
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

#
# Macro tables.
#

from __future__ import print_function

import re
import os
import string

import error
import path

#
# Macro tables
#
class macros:

    class macro_iterator:
        def __init__(self, keys):
            self.keys = keys
            self.index = 0

        def __iter__(self):
            return self

        def __next__(self):
            if self.index < len(self.keys):
                key = self.keys[self.index]
                self.index += 1
                return key
            raise StopIteration

        def iterkeys(self):
            return self.keys

    @staticmethod
    def _unicode_to_str(us):
        try:
            if type(us) == unicode:
                return us.encode('ascii', 'replace')
        except:
            pass
        try:
            if type(us) == bytes:
                return us.encode('ascii', 'replace')
        except:
            pass
        return us

    def __init__(self, name = None, original = None, sbdir = '.'):
        self.files = []
        self.macro_filter = re.compile(r'%{[^}]+}')
        if original is None:
            self.macros = {}
            self.read_maps = []
            self.read_map_locked = False
            self.write_map = 'global'
            self.macros['global'] = {}
            self.macros['global']['_cwd'] = ('dir', 'required', path.abspath(os.getcwd()))
            self.macros['global']['_sbdir'] = ('dir', 'required', path.abspath(sbdir))
            self.macros['global']['_sbtop'] = ('dir', 'required', path.abspath(path.dirname(sbdir)))
        else:
            self.macros = {}
            for m in original.macros:
                if m not in self.macros:
                    self.macros[m] = {}
                for k in original.macros[m]:
                    self.macros[m][k] = original.macros[m][k]
            self.read_maps = sorted(original.read_maps)
            self.read_map_locked = original.read_map_locked
            self.write_map = original.write_map
        if name is not None:
            self.load(name)

    def __copy__(self):
        return macros(original = self)

    def __str__(self):
        text_len = 80
        text = ''
        for f in self.files:
            text += '> %s%s' % (f, os.linesep)
        maps = sorted(self.macros)
        maps.remove('global')
        maps += ['global']
        for map in maps:
            text += '[%s]%s' % (map, os.linesep)
            for k in sorted(self.macros[map].keys()):
                d = self.macros[map][k]
                text += " %s:%s '%s'%s '%s'%s" % \
                    (k, ' ' * (20 - len(k)),
                     d[0], ' ' * (8 - len(d[0])),
                     d[1], ' ' * (10 - len(d[1])))
                if len(d[2]) == 0:
                    text += "''%s" % (os.linesep)
                else:
                    if '\n' in d[2]:
                        text += "'''"
                    else:
                        text += "'"
                indent = False
                ds = d[2].split('\n')
                lc = 0
                for l in ds:
                    lc += 1
                    l = self._unicode_to_str(l)
                    while len(l):
                        if indent:
                            text += ' %21s %10s %12s' % (' ', ' ', ' ')
                        text += l[0:text_len]
                        l = l[text_len:]
                        if len(l):
                            text += ' \\'
                        elif lc == len(ds):
                            if len(ds) > 1:
                                text += "'''"
                            else:
                                text += "'"
                        text += '%s' % (os.linesep)
                        indent = True
        return text

    def __iter__(self):
        return macros.macro_iterator(list(self.keys()))

    def __getitem__(self, key):
        macro = self.get(key)
        if macro is None or macro[1] == 'undefine':
            raise IndexError('key: %s' % (key))
        return macro[2]

    def __setitem__(self, key, value):
        key = self._unicode_to_str(key)
        if type(key) is not str:
            raise TypeError('bad key type (want str): %s (%s)' % (type(key), key))
        if type(value) is not tuple:
            value = self._unicode_to_str(value)
        if type(value) is str:
            value = ('none', 'none', value)
        if type(value) is not tuple:
            raise TypeError('bad value type (want tuple): %s' % (type(value)))
        if len(value) != 3:
            raise TypeError('bad value tuple (len not 3): %d' % (len(value)))
        value = (self._unicode_to_str(value[0]),
                 self._unicode_to_str(value[1]),
                 self._unicode_to_str(value[2]))
        if type(value[0]) is not str:
            raise TypeError('bad value tuple type field: %s' % (type(value[0])))
        if type(value[1]) is not str:
            raise TypeError('bad value tuple attrib field: %s' % (type(value[1])))
        if type(value[2]) is not str:
            raise TypeError('bad value tuple value field: %s' % (type(value[2])))
        if value[0] not in ['none', 'triplet', 'dir', 'file', 'exe']:
            raise TypeError('bad value tuple (type field): %s' % (value[0]))
        if value[1] not in ['none', 'optional', 'required',
                            'override', 'undefine', 'convert']:
            raise TypeError('bad value tuple (attrib field): %s' % (value[1]))
        if value[1] == 'convert':
            value = (value[0], value[1], self.expand(value[2]))
        self.macros[self.write_map][self.key_filter(key)] = value

    def __delitem__(self, key):
        self.undefine(key)

    def __contains__(self, key):
        return self.has_key(self._unicode_to_str(key))

    def __len__(self):
        return len(list(self.keys()))

    def keys(self, globals = True):
        if globals:
            keys = list(self.macros['global'].keys())
        else:
            keys = []
        for rm in self.get_read_maps():
            for mk in self.macros[rm]:
                if self.macros[rm][mk][1] == 'undefine':
                    if mk in keys:
                        keys.remove(mk)
                else:
                    keys.append(mk)
        return sorted(set(keys))

    def has_key(self, key):
        key = self._unicode_to_str(key)
        if type(key) is not str:
            raise TypeError('bad key type (want str): %s' % (type(key)))
        if self.key_filter(key) not in list(self.keys()):
            return False
        return True

    def create_map(self, _map):
        if _map not in self.macros:
            self.macros[_map] = {}

    def delete_map(self, _map):
        if _map in self.macros:
            self.macros.pop(_map, None)

    def maps(self):
        return list(self.macros.keys())

    def map_keys(self, _map):
        if _map in self.macros:
            return sorted(self.macros[_map].keys())
        return []

    def map_num_keys(self, _map):
        return len(self.map_keys(_map))

    def get_read_maps(self):
        return [rm[5:] for rm in self.read_maps]

    def key_filter(self, key):
        if key.startswith('%{') and key[-1] == '}':
            key = key[2:-1]
        return key.lower()

    def parse(self, lines):

        def _clean(l):
            if '#' in l:
                l = l[:l.index('#')]
            if '\r' in l:
                l = l[:l.index('r')]
            if '\n' in l:
                l = l[:l.index('\n')]
            return l.strip()

        trace_me = False
        if trace_me:
            print('[[[[]]]] parsing macros')
        macros = { 'global': {} }
        map = 'global'
        lc = 0
        state = 'key'
        token = ''
        macro = []
        for l in lines:
            lc += 1
            #print 'l:%s' % (l[:-1])
            if len(l) == 0:
                continue
            l = self._unicode_to_str(l)
            l_remaining = l
            for c in l:
                if trace_me:
                    print(']]]]]]]] c:%s(%d) s:%s t:"%s" m:%r M:%s' % \
                        (c, ord(c), state, token, macro, map))
                l_remaining = l_remaining[1:]
                if c == '#' and not state.startswith('value'):
                    break
                if c == '\n' or c == '\r':
                    if not (state == 'key' and len(token) == 0) and \
                            not state.startswith('value-multiline'):
                        raise error.general('malformed macro line:%d: %s' % (lc, l))
                if state == 'key':
                    if c not in string.whitespace:
                        if c == '[':
                            state = 'map'
                        elif c == '%':
                            state = 'directive'
                        elif c == ':':
                            macro += [token]
                            token = ''
                            state = 'attribs'
                        elif c == '#':
                            break
                        else:
                            token += c
                elif state == 'map':
                    if c == ']':
                        if token not in macros:
                            macros[token] = {}
                        map = token
                        token = ''
                        state = 'key'
                    elif c in string.printable and c not in string.whitespace:
                        token += c
                    else:
                        raise error.general('invalid macro map:%d: %s' % (lc, l))
                elif state == 'directive':
                    if c in string.whitespace:
                        if token == 'include':
                            self.load(_clean(l_remaining))
                            token = ''
                            state = 'key'
                            break
                    elif c in string.printable and c not in string.whitespace:
                        token += c
                    else:
                        raise error.general('invalid macro directive:%d: %s' % (lc, l))
                elif state == 'include':
                    if c is string.whitespace:
                        if token == 'include':
                            state = 'include'
                    elif c in string.printable and c not in string.whitespace:
                        token += c
                    else:
                        raise error.general('invalid macro directive:%d: %s' % (lc, l))
                elif state == 'attribs':
                    if c not in string.whitespace:
                        if c == ',':
                            macro += [token]
                            token = ''
                            if len(macro) == 3:
                                state = 'value-start'
                        else:
                            token += c
                elif state == 'value-start':
                    if c == "'":
                        state = 'value-line-start'
                elif state == 'value-line-start':
                    if c == "'":
                        state = 'value-multiline-start'
                    else:
                        state = 'value-line'
                        token += c
                elif state == 'value-multiline-start':
                    if c == "'":
                        state = 'value-multiline'
                    else:
                        macro += [token]
                        state = 'macro'
                elif state == 'value-line':
                    if c == "'":
                        macro += [token]
                        state = 'macro'
                    else:
                        token += c
                elif state == 'value-multiline':
                    if c == "'":
                        state = 'value-multiline-end'
                    else:
                        token += c
                elif state == 'value-multiline-end':
                    if c == "'":
                        state = 'value-multiline-end-end'
                    else:
                        state = 'value-multiline'
                        token += "'" + c
                elif state == 'value-multiline-end-end':
                    if c == "'":
                        macro += [token]
                        state = 'macro'
                    else:
                        state = 'value-multiline'
                        token += "''" + c
                else:
                    raise error.internal('bad state: %s' % (state))
                if state == 'macro':
                    macros[map][self._unicode_to_str(macro[0].lower())] = \
                                (self._unicode_to_str(macro[1]),
                                 self._unicode_to_str(macro[2]),
                                 self._unicode_to_str(macro[3]))
                    macro = []
                    token = ''
                    state = 'key'
        for m in macros:
            if m not in self.macros:
                self.macros[m] = {}
            for mm in macros[m]:
                self.macros[m][mm] = macros[m][mm]

    def load(self, name):
        names = self.expand(name).split(':')
        for n in names:
            if path.exists(n):
                try:
                    mc = open(path.host(n), 'r')
                    macros = self.parse(mc)
                    mc.close()
                    self.files += [n]
                    return
                except IOError as err:
                    pass
        raise error.general('opening macro file: %s' % \
                                (path.host(self.expand(name))))

    def get(self, key, globals = True, maps = None):
        key = self._unicode_to_str(key)
        if type(key) is not str:
            raise TypeError('bad key type: %s' % (type(key)))
        key = self.key_filter(key)
        if maps is None:
            maps = self.get_read_maps()
        else:
            if type(maps) is str:
                maps = [maps]
            if type(maps) != list:
                raise TypeError('bad maps type: %s' % (type(map)))
        for rm in maps:
            if key in self.macros[rm]:
                return self.macros[rm][key]
        if globals and key in self.macros['global']:
            return self.macros['global'][key]
        return None

    def get_type(self, key):
        m = self.get(key)
        if m is None:
            return None
        return m[0]

    def get_attribute(self, key):
        m = self.get(key)
        if m is None:
            return None
        return m[1]

    def get_value(self, key):
        m = self.get(key)
        if m is None:
            return None
        return m[2]

    def overridden(self, key):
        return self.get_attribute(key) == 'override'

    def define(self, key, value = '1'):
        self.__setitem__(key, ('none', 'none', value))

    def undefine(self, key):
        key = self._unicode_to_str(key)
        if type(key) is not str:
            raise TypeError('bad key type: %s' % (type(key)))
        key = self.key_filter(key)
        for map in self.macros:
            if key in self.macros[map]:
                del self.macros[map][key]

    def defined(self, key, globals = True, maps = None):
        return self.get(key, globals, maps) is not None

    def expand(self, _str):
        """Simple basic expander of config file macros."""
        _str = self._unicode_to_str(_str)
        expanded = True
        while expanded:
            expanded = False
            for m in self.macro_filter.findall(_str):
                name = m[2:-1]
                macro = self.get(name)
                if macro is None:
                    raise error.general('cannot expand default macro: %s in "%s"' %
                                        (m, _str))
                _str = _str.replace(m, macro[2])
                expanded = True
        return _str

    def find(self, regex, globals = True):
        what = re.compile(regex)
        keys = []
        for key in self.keys(globals):
            if what.match(key):
                keys += [key]
        return keys

    def set_read_map(self, _map):
        if not self.read_map_locked:
            if _map in self.macros:
                if _map not in self.get_read_maps():
                    rm = '%04d_%s' % (len(self.read_maps), _map)
                    self.read_maps = sorted(self.read_maps + [rm])
                return True
        return False

    def unset_read_map(self, _map):
        if not self.read_map_locked:
            if _map in self.get_read_maps():
                for i in range(0, len(self.read_maps)):
                    if '%04d_%s' % (i, _map) == self.read_maps[i]:
                        self.read_maps.pop(i)
                return True
        return False

    def set_write_map(self, map):
        if map in self.macros:
            self.write_map = map
            return True
        return False

    def unset_write_map(self):
        self.write_map = 'global'

    def lock_read_map(self):
        self.read_map_locked = True

    def unlock_read_map(self):
        self.read_map_locked = False

if __name__ == "__main__":
    import copy
    import sys
    m = macros()
    d = copy.copy(m)
    m['test1'] = 'something'
    if 'test1' in d:
        print('error: copy failed.')
        sys.exit(1)
    m.parse("[test]\n" \
            "test1: none, undefine, ''\n" \
            "name:  none, override, 'pink'\n")
    print('set test:', m.set_read_map('test'))
    if m['name'] != 'pink':
        print('error: override failed. name is %s' % (m['name']))
        sys.exit(1)
    if 'test1' in m:
        print('error: map undefine failed.')
        sys.exit(1)
    print('unset test:', m.unset_read_map('test'))
    print(m)
    print(list(m.keys()))
